package de.uka.ipd.sdq.reliability.solver.adaptivity;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import de.uka.ipd.sdq.markov.Label;
import de.uka.ipd.sdq.markov.MarkovChain;
import de.uka.ipd.sdq.markov.State;
import de.uka.ipd.sdq.markov.StateType;
import de.uka.ipd.sdq.markov.Transition;
import de.uka.ipd.sdq.markov.impl.LabelImpl;
import de.uka.ipd.sdq.pcmsolver.runconfig.PCMSolverWorkflowRunConfiguration;
import de.uka.ipd.sdq.reliability.solver.SolverPlugin;
import de.uka.ipd.sdq.reliability.solver.pcm2markov.MarkovBuilder;
import de.uka.ipd.sdq.reliability.solver.pcm2markov.MarkovTransformationResult;

public class AdaptivityAnalysis {

	private MarkovChain chain;
	private String simplifiedModelfile;
	private String sensitivityFile;
	private String matlabExecutable = null;
	private MarkovBuilder builder;
	
	private Map<String, String> variableTypes;
	private Map<String, String> variableNames;
	private Map<String, String> variableBehaviour;
	private Map<String, String> variableAttribute;
	private Map<String, String> maxWorkload;
	private Map<String, String> compressionRate;	
	private Map<String, Float> costPerVM;
	private Map<String, ArrayList<Float>> scaleUpBound;
	private Map<String, ArrayList<Float>> scaleDownBound;
	private Map<String, Float> startupTime;	
	private Map<Transition,String> variables;

	private MatlabConfiguration conf;
	private String simulationFolder;	
	private boolean sensitivityFileProvided = true;

	private String matlab_files_dir;
	
	private int controlVariables = 0;
	private int noncontrolVariables = 0;

	public AdaptivityAnalysis(MarkovTransformationResult result,PCMSolverWorkflowRunConfiguration configuration) {
		simplifiedModelfile = resolveFile(configuration.getMarkovSimplifiedModelFile());
		simulationFolder =  resolveFile(configuration.getMatlabOutput());
		sensitivityFile = resolveFile(configuration.getSensitivityModelFileName());
		if (configuration.isMatlabSimulationEnabled())
			matlabExecutable = resolveFile(configuration.getMatlabExecutable());
		if (configuration.getSensitivityModelFileName().equals(""))
			sensitivityFileProvided = false;
		builder = new MarkovBuilder(configuration.isMarkovModelTracesEnabled());		
		chain = builder.copyMarkovChain(result.getResultChain());
		
		variableTypes = new HashMap<String, String>();
		variableNames = new HashMap<String, String>();
		variables = new HashMap<Transition, String>();
		variableBehaviour = new HashMap<String, String>();
		variableAttribute = new HashMap<String, String>();
		maxWorkload = new HashMap<String, String>();
		compressionRate = new HashMap<String, String>();
		costPerVM = new HashMap<String, Float>();
		scaleUpBound = new HashMap<String, ArrayList<Float>>();
		scaleDownBound = new HashMap<String, ArrayList<Float>>();				
		startupTime = new HashMap<String, Float>();
		conf = new MatlabConfiguration();

	}

	
	public  void SaveModelforDotty(int i){
		String tmp = simplifiedModelfile;
		simplifiedModelfile += ""+i;
		SaveModelforDotty();
		
		try {
				Runtime rt = Runtime.getRuntime();
				rt.exec("dot -Tpng "+simplifiedModelfile+" -o "+simplifiedModelfile+".png");
		} catch (IOException e) {
			e.printStackTrace();
		}
		simplifiedModelfile = tmp;
	}
	
	public  void SaveModelforDotty() {

		builder.simplifyNames(chain, variableTypes);

		try {
			File f = new File(simplifiedModelfile);
			if (f.exists()) {
				f.delete(); // delete current (old) file
				f.createNewFile(); // create a new, empty file
			}
			BufferedWriter out = new BufferedWriter(new FileWriter(f, true));
			out.write("digraph G {\n");
			String transitions = "";
			for (Transition t : chain.getTransitions()) {
				String fromLabel = null;
				String toLabel = null;
				for (Label l : t.getFromState().getLabels())
					if (l.getKey().equals("SimplifiedName"))
						fromLabel = l.getValue();
				for (Label l : t.getToState().getLabels())
					if (l.getKey().equals("SimplifiedName"))
						toLabel = l.getValue();

				String label = null;
				if(variables.containsKey(t))				
					label =" [label=\""+ variables.get(t) +"\"]";
				else
					label = " [label=\"" + t.getProbability() + "\"]";												

				transitions += "\t" + fromLabel + " -> " + toLabel + label	+ ";\n";
			}
			out.write(transitions);
			out.write("\n}");
			out.close();
		} catch (IOException e) {

		}

	}

	public void initSimulation() {

		// parse sensitivity file and fill hashmaps
		if(sensitivityFileProvided)
			parseXmlFile(sensitivityFile);			
		// link hashmaps with chain states		
		for (State s : chain.getStates()) {			

			//initialize a label for the state to put the ID
			Label IDLabel = new LabelImpl(); 
			IDLabel.setKey("ID");
			String id = null;
			
			//search the id in case it's a failure state
			for (Label l : s.getLabels())
				if (l != null && l.getKey().equals("FailureTypeId")) {
					id = l.getValue();
					id = id.substring(id.indexOf('/') + 1, id.length());		
					variableNames.put(id,"a"+noncontrolVariables);
					noncontrolVariables++;
				}

			//search it in case it's a controlled decision state (get only the start of the branch)
			if (id == null && s.getName().contains("[") && s.getName().contains("Start")) {
				id = s.getName();
				id = id.substring(id.indexOf("[") + 1,id.indexOf("]"));
				if(variableTypes.containsKey(id)){
					variableNames.put(id,"C"+controlVariables);
					controlVariables++;
				}
			}
			
			
			//if it's a variable then save the id in the label
			if(id != null && variableTypes.keySet().contains(id)){
				IDLabel.setValue(id);
				s.getLabels().add(IDLabel);
			}				
		}
	}

	public void SimplifyChain() {
		builder.reduceProb1Transitions(chain, variableTypes.keySet());		     
	}



	/**
	 * Resolves a file's path in case it starts with "platform:/" and returns
	 * the entire absolute path to the file, including the file's name.
	 * 
	 * @param fileURL
	 *            the path to a file, including the file's name (and its
	 *            extension)
	 * @return the absolute path to the file, including the file's name
	 */
	private String resolveFile(String fileURL) {
		// if this is a platform URL, first resolve it to an absolute path
		if (fileURL.startsWith("platform:")) {
			try {
				URL solvedURL = FileLocator.resolve(new URL(fileURL));
				fileURL = solvedURL.getPath();
			} catch (Exception e) {
				e.printStackTrace();
				return "";
			}
		}
		return fileURL;

	}

	private static String locateFile(String bundle, String fullPath) {
		try {
			URL url = FileLocator.find(Platform.getBundle(bundle), new Path(
					fullPath), null);
			if (url != null)
				return FileLocator.resolve(url).getPath();
		} catch (Exception e) {
			e.printStackTrace();
			return "";
		}
		return null;
	}

	// Parser for the sensitivity xml
	private void parseXmlFile(String filename) {
		// get the factory
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		Document dom = null;

		try {
			// Using factory get an instance of document builder
			DocumentBuilder db = dbf.newDocumentBuilder();
			// parse using builder to get DOM representation of the XML file
			dom = db.parse(filename);

		} catch (Exception e) {
			e.printStackTrace();
		}

		// get all parameter nodes
		NodeList parameterList = dom.getDocumentElement().getElementsByTagName("childParameters__CombinedSensitivityParameter");
		if (parameterList != null && parameterList.getLength() > 0) {
			for (int i = 0; i < parameterList.getLength(); i++) {
				Element el = (Element) parameterList.item(i);
				// get user inputs
				ArrayList<String> inputs = getStringValues(el);
				String id = null;

				// Controlled Variable Case
				if (el.getAttribute("xsi:type").equals(
						"sensitivity:ProbabilisticBranchParameter")) {
					id = el.getAttribute("probabilisticBranchTransition__ProbabilisticBranchParameter");
					id = id.substring(id.indexOf('_'), id.length());
					variableTypes.put(id, "ctrl");
				}
				
				
				// non controlled variable case
				else if (el.getAttribute("xsi:type").equals("sensitivity:SoftwareFailureTypesParameter")) {
					id = el.getAttribute("softwareInducedFailureTypes__SoftwareFailureTypeReliabilityParameter");
					id = id.substring(id.indexOf('_'), id.length());
					variableTypes.put(id, "non_ctrl");
					//parse the node type ("internal", "autoscaling")
					variableAttribute.put(id, inputs.get(0));
					//parse the real reliability
					variableBehaviour.put(id, inputs.get(1));
					//parse compression rate
					compressionRate.put(id, inputs.get(2));
					//max workload
					maxWorkload.put(id, inputs.get(3));				
					//if it's an autoscaling node parse remaining fields
					if(inputs.get(0).equalsIgnoreCase("autoscaling")){												
						costPerVM.put(id,Float.parseFloat(inputs.get(4)));
						startupTime.put(id,Float.parseFloat(inputs.get(5)));
						scaleUpBound.put(id,parseScaleBound(inputs.get(6)));
						scaleDownBound.put(id,parseScaleBound(inputs.get(7)));						
					}					
				}
				
				// goal function
				else if (el.getAttribute("xsi:type").equals("sensitivity:VariableUsageParameter")){					
					variableBehaviour.put("payload", inputs.get(0));
					if(inputs.size()>1)
						variableBehaviour.put("goal", inputs.get(1));
				}
			}
		}
		//if a goal has not been specified, the default case is to get full availability
		if(!variableBehaviour.containsKey("goal"))
			variableBehaviour.put("goal","1");
	}
	
	private ArrayList<Float> parseScaleBound(String input){
		String tokens[] = input.split(" ");
		ArrayList<Float> values = new ArrayList<Float>();
		for(String s:tokens)
			values.add(Float.parseFloat(s));
		return values;
	}

	private ArrayList<String> getStringValues(Element elem) {
		ArrayList<String> values = new ArrayList<String>();
		NodeList nl = elem.getElementsByTagName("stringValues");
		if (nl != null && nl.getLength() > 0)
			for (int i = 0; i < nl.getLength(); i++)
				values.add(nl.item(i).getFirstChild().getNodeValue());
		return values;
	}

	public void buildMatlabFile() {

		//initialize matlab configuration
		conf.init(chain, 
				builder, 
				variableTypes, 
				variableNames, 
				variableBehaviour, 
				variableAttribute, 
				maxWorkload, 
				compressionRate,
				costPerVM, 
				scaleUpBound, 
				scaleDownBound, 
				startupTime,
				variables,
				noncontrolVariables);

		String matlab_templates_dir = locateFile(SolverPlugin.PLUGIN_ID,"input/matlab_templates");
		matlab_files_dir = simulationFolder + "/matlab_files";
		File matlab_templates_dir_file = new File(matlab_templates_dir);

		FilenameFilter filter = new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".m");
			}
		};
		String[] children = matlab_templates_dir_file.list(filter);

		new File(matlab_files_dir).mkdirs();

		for (String t_file : children)
			printMatlabFile(matlab_templates_dir + "/" + t_file,
					matlab_files_dir + "/" + t_file);

	}

	private void printMatlabFile(String input_file, String output_file) {
		try {
			
			FileInputStream fstream = new FileInputStream(input_file);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));

			Pattern pattern = Pattern.compile("<[a-zA-Z_]+>");
			Matcher matcher;
			String line;
			StringBuffer sb = new StringBuffer();
			while ((line = br.readLine()) != null) {
				matcher = pattern.matcher(line);
				while (matcher.find()) {
					String match = matcher.group();
					String key = match.substring(1, match.length() - 1);
					String replacement = conf.getValue(key);
					matcher.appendReplacement(sb,
							Matcher.quoteReplacement(replacement));
				}
				matcher.appendTail(sb);
				sb.append("\n");
			}

			FileWriter fileWriter = new FileWriter(output_file);
			BufferedWriter bw = new BufferedWriter(fileWriter);
			bw.write(sb.toString());
			bw.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void launchMatlabSimulation() {
		try {
			if (matlabExecutable != null) {
				Runtime rt = Runtime.getRuntime();
				System.out.println("executing: matlab -r \"run "+ matlab_files_dir + "/main.m\"");
				rt.exec("matlab -r \"run '" + matlab_files_dir+ "/main.m'\"");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	public void expandSuccesState() {
		builder.expandSuccessState(chain);
	}

	public void promoteIDs() {
		HashSet<State> visited = new HashSet<State>();
		for(Transition t:chain.getTransitions()){
			//if the transitions has already been pushed up i don't have to push it further
			if(!visited.contains(t.getToState())){			
				Label tmp = null;
				//search for state ID
				for(Label l:t.getToState().getLabels())
					if(l.getKey().equals("ID"))
						tmp = l;
				//if found pass it to it's predecessor
				if(tmp !=null){
					t.getToState().getLabels().remove(tmp);
					t.getFromState().getLabels().add(tmp);
					visited.add(t.getFromState());
				}
			}
		}

	}

	public void closeFinalStates() {
		List<State> failures = builder.getFailureStates(chain);
		builder.addAutoLoop(chain, failures);
		builder.addAutoLoop(chain, builder.getSuccessState(chain));
	}

	public void addSingleFailureState() {
		//build a list of all states without arcs to failure states
		List<State> withoutFailurestate = chain.getStates();
		for(Transition t:chain.getTransitions())
			if(t.getToState().getType().equals(StateType.FAILURE) && withoutFailurestate.contains(t.getFromState()))
				withoutFailurestate.remove(t.getToState());
		builder.addFailurePit(chain, withoutFailurestate);

	}

	public void shiftNonControlled() {
		ArrayList<Transition> oldTransition = new ArrayList<Transition>();
		//get old states infos
		for(Transition t:chain.getTransitions())
			for(Label l:t.getToState().getLabels())
				if(l.getKey().equals("ID") && variableTypes.containsKey(l.getValue()) && variableTypes.get(l.getValue()).equals("non_ctrl")){
					oldTransition.add(t);
				}
		
		//find the new states and switch ids		
		for(Transition t1:oldTransition){
			for(Transition t2:chain.getTransitions()){
				if(t2.getFromState().equals(t1.getFromState()) &&!t2.getToState().equals(t1.getToState())){
					Label tmp = null;
					for(Label l:t1.getToState().getLabels())
						if(l.getKey().equals("ID"))
							tmp=l;						
					LabelImpl newLabel = new LabelImpl();
					newLabel.setKey(tmp.getKey());
					newLabel.setValue(tmp.getValue());
					t2.getToState().getLabels().add(newLabel);
				}
			}
		}
		
		for(Transition t:oldTransition){
			Label lab = null;
			for(Label l:t.getToState().getLabels())
				if(l.getKey().equals("ID"))
					lab = l;
			t.getToState().getLabels().remove(lab);				 		
		}
		
	}

	public void addLimitedFailureState() {
		
		ArrayList<Transition> toAdd = new ArrayList<Transition>();
		ArrayList<String> ids = new ArrayList<String>();
		ArrayList<State> added = new ArrayList<State>();
		
		//get state with finite processing capabilities
		for(Transition t:chain.getTransitions()){
			for(Label l:t.getFromState().getLabels())
				if(l.getKey().equals("ID") && maxWorkload.containsKey(l.getValue())&& !maxWorkload.get(l.getValue()).equalsIgnoreCase("Inf")){					
					ids.add(l.getValue());
					toAdd.add(t);
				}
		}
		//add failure state to them
		for(Transition t:toAdd){
			if(added.contains(t.getFromState()))
				continue;
			
			//add an intermediate state
			State intermediate = builder.addIntermediateState(chain, t.getFromState());
			
			//add the failure state to the chain
			builder.addFailurePit(chain,t.getFromState());
			//add the father of the already considered list so we don't add any more failure states to it
			added.add(t.getFromState());	
			
			String id = getId(intermediate);
			
			//add a non controlled variable with name a(next free index)
			variableNames.put(id, "a"+noncontrolVariables);
			noncontrolVariables++;
			variableTypes.put(id, "non_ctrl");
			//parse the real reliability
			variableBehaviour.put(id, "1");			
		}
	}

	public void addSimulationVariables() {
		int IDcounter = 0; 
		for(State s:chain.getStates()){
			String id = getId(s);			
			boolean needVairable = true;
			//if the state already has a variable attached to it skip it
			if((s.equals(builder.getStartState(chain)))||id!=null && variableNames.containsKey(id))
				continue;
			//if state does not have a variable attached to it search in siblings 
			if(id == null){
				ArrayList<State> siblings = builder.getSiblings(chain, s);				
				for(State sibling:siblings){
					String siblingID = getId(sibling);
					if(siblingID != null && variableNames.containsKey(siblingID)){
						needVairable = false;
						break; //skip other siblings
					}
				}

				//if needVariable is still true i've to add a new variable for the state
				if(needVairable){
					//otherwise build a new one and add it to the toState (successful)
					if(id==null){
						Label l = new LabelImpl();
						id = "simulationVariable"+IDcounter;
						IDcounter++;						
						l.setKey("ID");			
						l.setValue(id);
						s.getLabels().add(l);
					}
					//add a non controlled variable with name a(next free index)
					variableNames.put(id, "a"+noncontrolVariables);
					noncontrolVariables++;
					variableTypes.put(id, "non_ctrl");
					//parse the real reliability
					variableBehaviour.put(id, "1");
				}
			}
		}
	}

	private String getId(State s){
		for(Label l:s.getLabels())
			if(l.getKey().equals("ID"))
				return l.getValue();
		return null;
	}

	public void moveVariablesToTransitions() {
		ArrayList<String> names = new ArrayList<String>();		
		for(Transition t:chain.getTransitions()){
			String toId = getId(t.getToState());
			if(toId != null && variableNames.containsKey(toId)){
				String name = variableNames.get(toId);
				if(names.contains(name)){
					name = "a"+noncontrolVariables;
					noncontrolVariables++;					
				}
				names.add(name);
				variables.put(t,name);
				continue;
			}			
		}		
	}



	public void promoteAvailabilities() {
		ArrayList<State> updated = new ArrayList<State>();
		int IDcounter = 0;
		for(Transition t:chain.getTransitions()){
			String id = getId(t.getToState());
			if(id != null && variableBehaviour.containsKey(id) && !updated.contains(t.getToState())){
				String availability = variableBehaviour.remove(id);
				if(getId(t.getFromState())==null){
					Label l = new LabelImpl();
					id = "availabilityVar"+IDcounter;
					IDcounter++;						
					l.setKey("ID");			
					l.setValue(id);
					t.getFromState().getLabels().add(l);
				}
				variableBehaviour.put(getId(t.getFromState()), availability);
				updated.add(t.getFromState());
			}
				
		}
		
	}
}


